Skip to main content

第 3 章:組態檔中的資源 resource

資源透過 resource 區塊來定義,一個 resource 可以定義一個或多個基礎設施資源對象,例如 VPC、虛擬機,或是 DNS 記錄、Consul 的鍵值對資料等。

資源語法

資源透過 resource 塊定義,我們先來講解透過 resource 塊定義單一資源物件的場景。

resource "aws_instance" "web" {
ami = "ami-a1b2c3d4"
instance_type = "t2.micro"
}
  • 定義了虛擬機器所使用的鏡像 id 以及虛擬機器的尺寸。

資源參數

不同資源定義了不同的可賦值的屬性,官方文件稱之為參數(Argument),有些參數是必填的,有些參數是可選的。使用某項資源前可以透過閱讀相關文件來了解參數清單以及他們的意義、賦值的限制條件。

資源類型的文檔

每一個 Terraform Provider 都有自己的文檔,用以描述它所支援的資源類型種類,以及每種資源類型所支援的屬性清單。

大部分的公共 Provider 都是透過 Terraform Registry 連帶文件一起發布的。當我們在 Terraform Registry 網站上瀏覽 Provider 的頁面時,我們可以點擊 "Documentation" 連結來瀏覽相關文件。Provider 的文件都是版本化的,我們可以選擇特定版本的 Provider 文件。

note

Provider 文件曾經是直接託管在 terraform.io,有些 Provider 的文檔目前依然託管在那裡,但目前 Terraform Rregistry 才是所有公共 Provider 文檔的主站(唯一的例外是用來讀取其他 Terraform 狀態資料的內建的 terraform provider,它的文檔目前不在 Terraform Registry 上)。

資源的行為

一個 resource 區塊聲明了作者想要建立的一個確切的基礎設施對象,並且設定了各項屬性的值。如果我們正在編寫一個新的 Terraform 程式碼文件,那麼程式碼所定義的資源僅僅只在程式碼中存在,並沒有與之對應的實際的基礎設施資源存在。

對一組 Terraform 程式碼執行 terraform apply 可以建立、更新或銷毀實際的基礎設施對象,Terraform 會制定並執行變更計劃,以使得實際的基礎設施符合程式碼的定義。

每當 Terraform 依照一個 resource 塊創建了一個新的基礎設施對象,這個實際的對象的 id 會被保存進 Terraform 狀態中,使得將來 Terraform 可以根據變更計劃對它進行更新或是銷毀操作。如果一個 resource 區塊所描述的資源在狀態檔案中已有記錄,那麼 Terraform 會比對記錄的狀態與程式碼描述的狀態,如果有必要, Terraform 會制定變更計畫以使得資源狀態能夠符合程式碼的描述。

這種行為適用於所有資源而無關其類型。創造、更新、銷毀一個資源的細節會根據資源類型而不同,但是這個行為規則卻是普適的。

存取資源輸出屬性

資源不但可以透過參數傳值,成功創建的資源還對外輸出一些透過呼叫 API 才能獲得的唯讀數據,經常包含了一些我們在實際創建一個資源之前無法獲知的數據,比如雲主機的 id 等,官方文檔將之稱為屬性(Attribute)。我們可以在同一模組內的程式碼中引用資源的屬性來建立其他資源或是表達式。在表達式中引用資源屬性的語法是 <RESOURCE TYPE>.<NAME>.<ATTRIBUTE>

要取得一個資源類型輸出的屬性列表,我們可以查閱對應的 Provider 文檔,一般在文檔中會專門記錄資源的輸出屬性列表。

敏感的資源屬性

在為資源類型定義架構時,Provider 開發著可以將某些屬性標記為 sensitive,在這種情況下,Terraform 會在展示涉及該屬性的計畫時顯示佔位符標記 (sensitive) 而不是實際值。

標記為 sensitive 的 Provider 屬性的行為類似於宣告為 的 sensitive 輸入變量,Terraform 將隱藏計劃中的值,也將隱藏從該值派生出的任何其他敏感值。但是,該行為存在一些限制,如 Terraform 可能會暴露敏感變數。

如果使用資源屬性中的敏感值作為輸出值的一部分,Terraform 將要求將輸出值本身標記為 sensitive,以確認確實打算將其匯出。

Terraform 仍會在狀態中記錄敏感值,因此任何可以存取狀態資料的人都可以以明文形式存取敏感值。

資源的依賴關係

我們在介紹輸出值的 depends_on 時候已經簡單介紹過了依賴關係。一般來說在 Terraform 程式碼定義的資源之間不會有特定的依賴關係,Terraform 可以並行地對多個無依賴關係的資源執行變更,預設情況下這個並行度是 10。

然而,創建某些資源所需的資訊依賴於另一個資源創建後輸出的屬性,又或者必須在某些資源成功創建後才可以被創建,這時資源之間就存在依賴關係。

大部分資源間的依賴關係可以被 Terraform 自動處理,Terraform 會分析 resource 區塊內的表達式,根據表達式的引用鏈來決定資源之間的引用,進而計算出資源在建立、更新、銷毀時的執行順序。大部分情況下,我們不需要明確指定資源之間的依賴關係。

然而,有時候某些依賴關係是無法從程式碼推導出來的。例如,Terraform 必須要建立一個存取控制權限資源,以及另一個需要該權限才能成功建立的資源。後者的創建依賴於前者的成功創建,然而這種依賴在程式碼中沒有表現為資料引用關聯,在這種情況下,我們需要 depends_on 用來這種依賴關係。

元參數

resource 區塊支援幾種元參數聲明,這些元參數可以被聲明在所有類型的 resource 區塊內,它們將會改變資源的行為:

  • depends_on:明確聲明依賴關係
  • count:建立多個資源實例
  • for_each:迭代集合,為集合中每個元素建立一個對應的資源實例
  • provider:指定非預設 Provider 實例
  • lifecycle:自訂資源的生命週期行為
  • provisioner 和 connection:在資源建立後執行一些額外的操作下面我們將逐一講解他們的用法。

depends_on

使用 depends_on 可以明確聲明資源之間哪些 Terraform 無法自動推導出的隱含的依賴關係。只有當資源間確實存在依賴關係,但是彼此間又沒有資料引用的場景下才有必要使用 depends_on

使用 depends_on 的例子是這樣的:

depends_on
resource "aws_iam_role" "example" {
name = "example"

# assume_role_policy is omitted for brevity in this example. See the
# documentation for aws_iam_role for a complete example.
assume_role_policy = "..."
}

resource "aws_iam_instance_profile" "example" {
# Because this expression refers to the role, Terraform can infer
# automatically that the role must be created first.
role = aws_iam_role.example.name
}

resource "aws_iam_role_policy" "example" {
name = "example"
role = aws_iam_role.example.name
policy = jsonencode({
"Statement" = [{
# This policy allows software running on the EC2 instance to
# access the S3 API.
"Action" = "s3:*",
"Effect" = "Allow",
}],
})
}

resource "aws_instance" "example" {
ami = "ami-a1b2c3d4"
instance_type = "t2.micro"

# Terraform can infer from this that the instance profile must
# be created before the EC2 instance.
iam_instance_profile = aws_iam_instance_profile.example

# However, if software running in this EC2 instance needs access
# to the S3 API in order to boot properly, there is also a "hidden"
# dependency on the aws_iam_role_policy that Terraform cannot
# automatically infer, so it must be declared explicitly:
depends_on = [
aws_iam_role_policy.example,
]
}

讓我們來分段解釋一下這個場景,首先我們聲明了一個 AWS IAM 角色,將角色綁定在一個主機實例設定檔上:

define aws iam role
resource "aws_iam_role" "example" {
name = "example"

# assume_role_policy is omitted for brevity in this example. See the
# documentation for aws_iam_role for a complete example.
assume_role_policy = "..."
}

resource "aws_iam_instance_profile" "example" {
# Because this expression refers to the role, Terraform can infer
# automatically that the role must be created first.
role = aws_iam_role.example.name
}

虛擬機器的聲明程式碼中的這個賦值使得 Terraform 能夠判斷虛擬機器依賴主機實例設定檔:

aws instance
resource "aws_instance" "example" {
ami = "ami-a1b2c3d4"
instance_type = "t2.micro"

# Terraform can infer from this that the instance profile must
# be created before the EC2 instance.
iam_instance_profile = aws_iam_instance_profile.example
}

至此,Terraform 規劃出的建立順序是 IAM 角色 → 主機實例設定檔 → 主機實例。但是我們又為這個 IAM 角色增加了對 S3 儲存服務的完全控制權限:

define aws iam role policy
resource "aws_iam_role_policy" "example" {
name = "example"
role = aws_iam_role.example.name
policy = jsonencode({
"Statement" = [{
# This policy allows software running on the EC2 instance to
# access the S3 API.
"Action" = "s3:*",
"Effect" = "Allow",
}],
})
}

也就是說,虛擬機器實例由於綁定了主機實例配置文件,從而在運行時擁有了一個 IAM 角色,而這個 IAM 角色又被賦予了 S3 的權限。但是虛擬機器實例的聲明程式碼中並沒有引用 S3 權限的任何輸出屬性,這將導致 Terraform 無法理解他們之間存在依賴關係,進而可能會並行地創建兩者,如果虛擬機器實例先創建了出來,內部的程式開始運作時,它所需要的 S3 權限卻還沒創建完成,那就會導致程式執行錯誤。為了確保虛擬機器創建時S3權限一定已經存在,我們可以用 depends_on 明確聲明它們的依賴關係:

dependency on the aws_iam_role_policy
# However, if software running in this EC2 instance needs access
# to the S3 API in order to boot properly, there is also a "hidden"
# dependency on the aws_iam_role_policy that Terraform cannot
# automatically infer, so it must be declared explicitly:
depends_on = [
aws_iam_role_policy.example,
]

depends_on 的賦值必須是包含同一模組內聲明的其他資源名稱的列表,不允許包含其他表達式,例如不允許使用其他資源的輸出屬性,這是因為 Terraform 必須在計算資源間關係之前就能理解列表中的值,為了能夠安全地完成表達式計算,所以限制只能使用資源實例的名稱。

depends_on 只能作為最後的手段使用,如果我們使用 depends_on,我們應該用註解記錄我們使用它的原因,以便今後程式碼的維護者能夠理解隱藏的依賴關係。

count

一般來說,一個 resource 區塊定義了一個對應的實際基礎設施資源物件。但是有時候我們希望創建多個相似的對象,例如創建一組虛擬機器。Terraform 提供了兩種方法來實現這個目標:count 與 for_each

count 參數可以是任意自然數,Terraform 會建立 count 資源實例,每個實例都對應了一個獨立的基礎設施對象,並且在執行 Terraform 程式碼時,這些對像是被分別建立、更新或銷毀的:

resource "aws_instance" "server" {
count = 4 # create four similar EC2 instances

ami = "ami-a1b2c3d4"
instance_type = "t2.micro"

tags = {
Name = "Server ${count.index}"
}
}

我們可以在 resource 區塊中的表達式裡使用 count 物件來取得目前的 count 索引號碼。count 物件只有一個屬性:

  • count.index:代表目前物件對應的 count 下標索引(從0開始)

如果一個 resource 區塊定義了 count 參數,那麼 Terraform 會把這種多資源實例物件與沒有 count 參數的單一資源實例物件區分開:

  • 存取單一資源實例物件:<TYPE>.<NAME>(例如:aws_instance.server)
  • 存取多資源實例物件:<TYPE>.<NAME>[<INDEX>](例如:aws_instance.server[0]aws_instance.server[1])

聲明了 count 或 for_each 的資源必須使用下標索引或鍵來存取。

count 參數可以是任意自然數,然而與 resource 的其他參數不同,count 的值在 Terraform 進行任何遠端資源操作(實際的增刪改查)之前必須是已知的,這也就意味著賦予 count 參數的表達式不可以引用任何其他資源的輸出屬性(例如由其他資源物件建立時傳回的一個唯一的 ID)。count 的表達式中可以引用來自 data 傳回的輸出屬性,只要 data 可以不依賴任何其他 resource 進行查詢。

for_each

for_each 是 Terraform 0.12.6 開始引進的新功能。一個 resource 區塊不允許同時聲明 count 與 for_eachfor_each 參數可以是一個 map 或是一個 set(string),Terraform 會為集合中每一個元素都創建一個獨立的基礎設施資源對象,和 count 一樣,每一個基礎設施資源對像在執行 Terraform 代碼時都是獨立創建、修改、銷毀的。

map
resource "azurerm_resource_group" "rg" {
for_each = {
a_group = "eastus"
another_group = "westus2"
}
name = each.key
location = each.value
}

set(string)
resource "aws_iam_user" "the-accounts" {
for_each = toset( ["Todd", "James", "Alice", "Dottie"] )
name = each.key
}

我們可以在聲明了 for_each 參數的 resource 區塊內使用 each 物件來存取目前的迭代器物件:

  • each.key:map 的鍵,或是 set 中的值
  • each.value:map 的值,或是 set 中的值如果 for_each 的值是一個 set,那麼 each.key 和 each.value 是相等的。

使用 for_each 時,map 的所有鍵、set 的所有 string 值都必須是已知的,也就是狀態檔中已有記錄的值。所以有時候我們可能需要在執行 terraform apply 時加入 -target 參數,實現逐步創建。另外,for_each 所使用的鍵集合不能夠包含或依賴非純函數,也就是重複執行會傳回不同傳回值的函數,例如 uuidbcrypttimestamp 等。

當一個 resource 宣告了 for_each 時,Terraform 會把這種多資源實例物件與沒有 count 參數的單一資源實例物件區分開:

  • 存取單一資源實例物件:<TYPE>.<NAME>(例如:aws_instance.server)
  • 存取多資源實例物件:<TYPE>.<NAME>[<KEY>](例如:aws_instance.server["ap-northeast-1"]aws_instance.server["ap-northeast-2"])

聲明了 count 或 for_each 的資源必須使用下標索引或鍵來存取。

由於 Terraform 沒有用以宣告 set 的字面量,所以我們有時需要使用 toset 函數把 list(string) 轉換為set(string):

toset
locals {
subnet_ids = toset([
"subnet-abcdef",
"subnet-012345",
])
}

resource "aws_instance" "server" {
for_each = local.subnet_ids

ami = "ami-a1b2c3d4"
instance_type = "t2.micro"
subnet_id = each.key # note: each.key and each.value are the same for a set

tags = {
Name = "Server ${each.key}"
}
}

這裡我們用 toset 把一個 list(string) 轉換成了 set(string),然後賦予 for_each。在轉換過程中,list 中所有重複的元素會被拋棄,只剩下不重複的元素,例如 toset(["b", "a", "b"]) 的結果只有 "a" 和 "b" ,而 set 的元素沒有特定順序。

如果我們要把一個輸入變數賦予 for_each,我們可以直接定義變數的型別約束來避免明確呼叫轉換 toset 類型:

for_each
variable "subnet_ids" {
type = set(string)
}

resource "aws_instance" "server" {
for_each = var.subnet_ids

# (and the other arguments as above)
}

在 for_each 和 count 之間選擇

如果創建的資源實例彼此之間幾乎完全一致,那麼 count 比較合適。如果彼此之間的參數差異無法直接從 count 的下標派生,那麼使用 for_each 會更加安全。

在 Terraform 引入 for_each 之前,我們經常使用 count.index 搭配 length 函數和 list 來建立多個資源實例:

for each & count
variable "subnet_ids" {
type = list(string)
}

resource "aws_instance" "server" {
# Create one instance for each subnet
count = length(var.subnet_ids)

ami = "ami-a1b2c3d4"
instance_type = "t2.micro"
subnet_id = var.subnet_ids[count.index]

tags = {
Name = "Server ${count.index}"
}
}

這種實作方法是脆弱的,因為資源仍然是以他們的下標而不是實際的 string 值來區分的。如果我們從 subnet_ids 清單的中間移除了一個元素,那麼從該位置起後續所有的 aws_instance 都會發現它們的 subnet_id 發生了變化,結果就是所有後續的 aws_instance 都需要更新。這種場景下如果使用 for_each 就更為妥當,如果使用 for_each,那麼只有被移除的 subnet_id 對應的 aws_instance 會被銷毀。

provider

關於 provider 的定義我們在前面介紹 Provider 的章節已經提到過了,如果我們聲明了同一類型 Provider 的多個實例,那麼我們在創建資源時可以透過指定 provider 參數選擇要使用的 Provider 實例。如果沒有指定 provider 參數,那麼 Terraform 預設使用資源類型名稱中第一個單字所對應的 Provider 實例,例如 google_compute_instance 的預設 Provider 實例就是 googleaws_instance 的預設 Provider 就是 aws

指定 provider 參數的例子:

provider
# default configuration
provider "google" {
region = "us-central1"
}

# alternate configuration, whose alias is "europe"
provider "google" {
alias = "europe"
region = "europe-west1"
}

resource "google_compute_instance" "example" {
# This "provider" meta-argument selects the google provider
# configuration whose alias is "europe", rather than the
# default configuration.
provider = google.europe

# ...
}

provider 參數期待的賦值是 <PROVIDER> 或是 <PROVIDER>.<ALIAS>,不需要雙引號。因為當 Terraform 開始計算依賴路徑圖時,provider 關係必須是已知的,所以除了這兩種以外的表達式是不被接受的。

lifecycle

通常一個資源物件的生命週期在前面「資源的行為」一節中已經描述了,但是我們可以用區塊來 lifecycle 定一個不一樣的行為方式,例如:

lifecycle
resource "azurerm_resource_group" "example" {
# ...

lifecycle {
create_before_destroy = true
}
}

lifecycle 區塊和它的內容都屬於元參數,可以被宣告於任意類型的資源區塊內部。Terraform 支援如下幾種 lifecycle

  • create_before_destroy(bool):預設情況下,當 Terraform 需要修改一個因為服務端API限製而無法直接升級的資源時,Terraform 會刪除現有資源對象,然後用新的組態參數建立新的資源對象取代之。create_before_destroy 參數可以修改這個行為,使得 Terraform 先建立新對象,只有在新對象成功建立並取代舊對象後再銷毀舊對象。這並不是預設的行為,因為許多基礎設施資源需要有一個唯一的名字或是別的什麼識別屬性,在新舊物件並存時也要符合這種約束。有些資源類型有特別的參數可以為每個物件名稱添加一個隨機的前綴以防止衝突。Terraform 不能預設採用這種行為,所以在使用 create_before_destroy 前你必須了解每一種資源類型在這方面的限制。
  • prevent_destroy(bool):這個參數是一個保險措施,只要它被設定 true 為時,Terraform 會拒絕執行任何可能會銷毀該基礎設施資源的變更計畫。這個參數可以預防意外刪除關鍵資源,例如錯誤地執行了 terraform destroy,或是意外修改了資源的某個參數,導致 Terraform 決定刪除並重建新的資源實例。在 resource 區塊內聲明了 prevent_destroy = true 會導致無法執行 terraform destroy,所以對它的使用要節制。需要注意的是,該措施無法防止我們刪除 resource 區塊後 Terraform 刪除相關資源,因為對應的 prevent_destroy = true 聲明也被一併刪除了。
  • ignore_changes(list(string)):預設情況下,Terraform 偵測到程式碼描述的配置與真實基礎設施對象之間有任何差異時都會計算一個變更計畫來更新基礎設施對象,使其符合程式碼描述的狀態。在一些非常罕見的場景下,實際的基礎設施物件會被 Terraform 以外的流程所修改,這就會使得 Terraform 不停地嘗試修改基礎設施物件以彌合和程式碼之間的差異。在這種情況下,我們可以透過設定 ignore_changes 來指示 Terraform 忽略某些屬性的變更。ignore_changes 的值定義了一組在創建時需要按照程式碼定義的值來創建,但在更新時不需要考慮值的變化的屬性名,例如:
igonre_changes
resource "aws_instance" "example" {
# ...

lifecycle {
ignore_changes = [
# Ignore changes to tags, e.g. because a management agent
# updates these based on some ruleset managed elsewhere.
tags,
]
}
}
  • 你也可以忽略 map 中特定的元素,例如 tags["Name"],但要注意的是,如果你是想忽略 map 中特定元素的變更,那麼你必須先確保 map 中含有這個元素。如果一開始 map 中並沒有這個鍵,而後外部系統加入了這個鍵,那麼 Terraform 還是會把它當成一次變更來處理。比較好的方法是你在程式碼中先為這個鍵建立一個佔位元素來確保這個鍵已經存在,這樣在外部系統修改了鍵對應的值以後 Terraform 會忽略這個變更。
tag
resource "aws_instance" "example" {
# ...

tags = {
# Initial value for Name is overridden by our automatic scheduled
# re-tagging process; changes to this are ignored by ignore_changes
# below.
Name = "placeholder"
}

lifecycle {
ignore_changes = [
tags["Name"],
]
}
}
  • 除了使用一個 list(string),也可以使用關鍵字 "all",這時 Terraform 會忽略資源一切屬性的變更,這樣Terraform 只會創建或銷毀一個對象,但絕不會嘗試更新一個對象。你只能在 ignore_changes 裡忽略所屬的 resource 的屬性,ignore_changes 不可以賦予它本身或是其他任何元參數。
  • replace_triggered_by(包含資源參考的清單):強制 Terraform 在引用的資源或是資源屬性發生變更時替換宣告該區塊的父資源,值為一個包含了託管資源、實例或是實例屬性參考表達式的清單。當宣告該區塊的資源宣告了 count 或是 for_each 時,我們可以在表達式中使用 count.index 或是 each.key 來指定引用實例的序號。

replace_triggered_by 可以在以下幾種場景中使用:

  • 如果表達式指向多實例的資源聲明(例如聲明了 count 或是 for_each 的資源),那麼這組資源中任意實例發生變更或被替換時都會引發聲明 replace_triggered_by 的資源被替換
  • 如果表達式指向單一資源實例,那麼該實例發生變更或被替換時將引發聲明 replace_triggered_by 的資源被替換
  • 如果表達式指向單一資源實例的單一屬性,那麼該屬性值的任何變更都會引發宣告 replace_triggered_by 的資源被替換

我們在 replace_triggered_by 中只能引用託管資源。這允許我們在不引發強制替換的前提下修改這些表達式。

lifecycle
resource "aws_appautoscaling_target" "ecs_target" {
# ...
lifecycle {
replace_triggered_by = [
# Replace `aws_appautoscaling_target` each time this instance of
# the `aws_ecs_service` is replaced.
aws_ecs_service.svc.id
]
}
}

lifecycle 配置影響了 Terraform 如何建構並遍歷相依圖。作為結果,lifecycle 內賦值僅支援字面量,因為它的計算過程發生在 Terraform 計算的極早期。這就是說,例如 prevent_destroycreate_before_destroy 的值只能是 true 或者 falseignore_changesreplace_triggered_by 的列表內只能是硬編碼的屬性名。

Precondition 與 Postcondition

在 lifecycle 區塊中聲明 precondition 與 postcondition 區塊可以為資源、資料來源以及輸出值建立自訂的驗證規則。

Terraform 在計算物件之前會先檢查該物件關聯的 precondition,並且在物件計算完成後執行 postcondition 檢查。Terraform 會盡可能早地執行自訂檢查,但如果表達式中包含了只有在 apply 階段才能知曉的值,那麼該檢查也將被推遲執行。

每一個 precondition 與 postcondition 區塊都需要一個 condition 參數。此參數是一個表達式,在滿足條件時返回 true,否則返回 false。此表達式可以引用同一模組內的任意其他對象,只要這種引用不會產生環依賴。在 postcondition 表達式中也可以使用 self 物件參考聲明 postcondition 的資源實例的屬性。

如果 condition 表達式計算結果為 false,Terraform 會產生錯誤訊息,包含了 error_message 表達式的內容。如果我們聲明了多個條 precondition 或 postcondition,Terraform 會傳回所有失敗條件對應的錯誤訊息。

下面的例子示範了透過 postcondition 偵測呼叫者是否不小心傳入了錯誤的 AMI 參數:

postcondition
data "aws_ami" "example" {
id = var.aws_ami_id

lifecycle {
# The AMI ID must refer to an existing AMI that has the tag "nomad-server".
postcondition {
condition = self.tags["Component"] == "nomad-server"
error_message = "tags[\"Component\"] must be \"nomad-server\"."
}
}
}

在 resource 或 data 區塊中的 lifecycle 區塊可以同時包含 precondition 與 postcondition 區塊。

  • Terraform 會在計算完 count 和 for_each 元參數後執行 precondition 區塊。這使得 Terraform 可以對每一個實例獨立進行檢查,並允許在表達式中使用 each.keycount.index 等。Terraform 也會在計算資源的參數表達式之前執行 precondition 檢查。precondition 可以用來防止參數表達式計算中的錯誤被激發。
  • Terraform 在計算和執行對一個託管資源的變更之後執行 postcondition 檢查,或在完成資料來源讀取後執行它關聯的 postcondition 檢查。postcondition 失敗會阻止其他依賴此失敗資源的其他資源的變更。

在大多數情況下,我們不建議在同一設定檔中同時包含表示同一個物件的 data 區塊和 resource 區塊。這樣做會使得 Terraform 無法理解 data 區塊的結果會被 resource 區塊的變更所影響。然而,當我們需要檢查一個 resource 區塊的結果,而恰巧該結果又沒有被資源直接輸出時,我們可以使用 data 區塊並在區塊中直接使用 postcondition 來檢查該物件。這等於是告訴 Terraform 該 data 區塊是用來檢查其他什麼地方定義的物件的,從而允許 Terrform 以正確的順序執行操作。

provisioner 和 connection

某些基礎設施物件需要在創建後執行特定的操作才能正式工作。比如說,主機實例必須在上傳了設定或是由設定管理工具初始化之後才能正常運作。

像這樣創建後執行的操作可以使用預置器(Provisioner)。預置器是由 Terraform 提供的另一組插件,每種預置器可以在資源物件建立後執行不同類型的操作。

使用預置器需要節制,因為他們採取的操作並非 Terraform 聲明式的風格,所以 Terraform 無法對他們執行的變更進行建模和保存。

預置器也可以聲明為資源銷毀前執行,但會有一些限制。

作為元參數,provisioner 和 connection 可以聲明在任意類型的 resource 區塊內。

connection
resource "aws_instance" "web" {
# ...

provisioner "file" {
source = "conf/myapp.conf"
destination = "/etc/myapp.conf"

connection {
type = "ssh"
user = "root"
password = var.root_password
host = self.public_ip
}
}
}

我們在 aws_instance 中定義了類型為的 file 預置器,該預置器可以本機檔案或資料夾拷貝到目標機器的指定路徑下。我們在預置器內部定義了 connection 區塊,類型是 ssh。我們對 connection 的 host 賦值 self.public_ip,在這裡 self 代表預置器所在的母塊,也就是 aws_instance.web,所以 self.public_ip 代表著 aws_instance.web.public_ip,也就是創建出來的主機的公網 ip。

file 類型預置器支援 ssh 和 winrm 兩種類型的 connection

預置器根據運行的時機分為兩種類型,創建時預置器以及銷毀時預置器。

建立時預置器 provisioner

預設情況下,資源物件被建立時會執行預置器,在物件更新、銷毀時則不會運作。預置器的預設行為時為了引導一個系統。

如果建立時預置器失敗了,那麼資源物件會被標記污點(我們將在介紹 terraform taint 指令時詳細介紹)。一個被標記污點的資源在下次執行 terraform apply 指令時會被銷毀並重建。Terrform的這種設計是因為當預設器運作失敗時標誌著資源處於半就緒的狀態。由於 Terraform 無法衡量預置器的行為,所以唯一能完全確保資源正確初始化的方式就是刪除重建。

我們可以透過設定 on_failure 參數來改變這種行為。

銷毀時預置器 provisioner

如果我們設定預置器的 when 參數為 destroy,那麼預置器會在資源被銷毀時執行:

destroy
resource "aws_instance" "web" {
# ...

provisioner "local-exec" {
when = destroy
command = "echo 'Destroy-time provisioner'"
}
}

銷毀時預置器在資源實際銷毀前運作。如果運行失敗,Terraform 會報錯,並在下次執行 terraform apply 操作時重新執行預置器。在這種情況下,需要仔細注意銷毀時預置器以使之能夠安全地重複執行。

銷毀時預置器只有在存在於程式碼中的情況下才會在銷毀時執行。如果一個 resource 區塊連帶內部的銷毀時預置器區塊一起被從程式碼中刪除,那麼被刪除的預置器在資源被銷毀時就不會被執行。要解決這個問題,我們需要使用多個步驟來繞過這個限制:

  • 修改資源聲明程式碼,新增 count = 0 參數
  • 執行 terraform apply,運行刪除時預置器,然後刪除資源實例
  • 刪除 resource 區塊
  • 重新執行 terraform apply,此時應該不會有任何變更需要執行

這個限制在未來將會得到解決,但目前來說我們必須節制使用銷毀時預置器。

預置器 provisioner 失敗行為

預設情況下,預置器運作失敗會導致 terraform apply 執行失敗。可以透過設定 on_failure 參數來改變這一行為。可以設定的值為:

  • continue:忽略錯誤,繼續執行創建或銷毀
  • fail:報錯並終止執行變更(這是預設行為)。如果這是一個建立時預置器,則在對應資源物件上標記污點範例:
on_failure
resource "aws_instance" "web" {
# ...

provisioner "local-exec" {
command = "echo The server's IP address is ${self.private_ip}"
on_failure = continue
}
}

本地資源

雖然大部分資源類型都對應的是透過遠端基礎設施 API 控制的一個資源對象,但也有一些資源對像他們只存在於 Terraform 進程自身內部,用來計算產生某些結果,並將這些結果保存在狀態中以備日後使用。

比如說,我們可以用 tls_private_key 生成公私鑰,用 tls_self_signed_cert 生成自簽名證書,或是用 random_id 生成隨機 id。雖然不像其他「真實」基礎設施物件般重要,但這些本地資源也可以成為連接其他資源有用的黏合劑。

本地資源的行為與其他類型資源是一致的,但是他們的結果資料僅存在於 Terraform 狀態檔案中。「銷毀」這種資源只是將結果資料從狀態中刪除。

操作超時設定

有些資源類型提供了特殊的 timeouts 內嵌塊參數,它允許我們配置我們允許操作持續多長時間,逾時將被認定為失敗。比如說,aws_db_instance 資源允許我們分別為 createupdatedelete 操作設定超時時間。

超時完全由資源對應的 Provider 來處理,但支援超時設定的 Provider 一般都遵循相同的傳統,那就是由一個名為 timeouts 的嵌入塊參數定義超時設置,timeouts 內可以分別設置不同操作的超時時間。超時時間由 string 描述,例如 "60m" 代表 60 分鐘,"10s" 代表 10 秒,"2h" 代表 2 小時。

timeouts
resource "aws_db_instance" "example" {
# ...

timeouts {
create = "60m"
delete = "2h"
}
}

可設定逾時的操作類別由每種支援逾時設定的資源類型自行決定。大部分資源類型不支援設定超時。使用超時前請先查閱相關文件。

資料來源 data

資料來源允許查詢或計算一些資料以供其他地方使用。使用資料來源可以讓 Terraform 程式碼使用在 Terraform 管理範圍之外的一些信息,或是讀取其他 Terraform 程式碼保存的狀態。

每一種 Provider 都可以在定義一些資源類型的同時定義一些資料來源。

使用資料來源

資料來源透過一種特殊的資源存取:data 資源。資料來源透過 data 塊聲明:

data
data "aws_ami" "example" {
most_recent = true

owners = ["self"]
tags = {
Name = "app-server"
Tested = "true"
}
}

一個 data 區塊請求 Terraform 從一個指定的資料來源 aws_ami 讀取指定資料並且把結果輸出到 Local Name 為的 example 實例中。我們可以在同一模組內的程式碼中透過資料來源名稱來引用資料來源,但無法從模組外部直接存取資料來源。

同資源類似,一個資料來源類型以及它的名稱一同構成了該資料來源的識別符,所以資料來源類型加名稱的組合在同一模組內必須是唯一的。

在 data 塊體(花括號中間的內容)是傳給資料來源的查詢條件。查詢條件參數的種類取決於資料來源的類型,在上述範例中,most_recentowners 和 tags 都是定義查詢 aws_ami 資料來源時所使用的查詢條件。

與資料來源這種特殊資源不同的是,我們在上一節介紹的主要資源(使用 resource 區塊定義的)是一種「託管資源」。這兩種資源都可以接收參數並對外輸出屬性,但託管資源會觸發 Terraform 對基礎設施物件進行增刪改操作,而資料來源只會觸發讀取操作。簡單來說,我們一般說的「資源」就是特別指託管資源。

資料來源參數

每一種資料來源資源都關聯到一個外部資料來源,資料來源類型決定了它接收的查詢參數以及輸出的資料。每一種資料來源類型都屬於一個 Provider。大部分 data 區塊內的資料來源參數都是由對應的資料來源類型定義的,這些參數的賦值可以使用完整的 Terraform 表達式能力或其他 Terraform 語言的功能。

然而類似資源,Terraform 也為所有類型的資料來源定義了一些元參數。這些元參數的限制和功能我們將在後續節當中敘述。

資料來源行為

如果資料來源的查詢參數涉及到的表達式只引用了字面量或是在執行時 terraform plan 就已知的資料(例如輸入變數),那麼資料來源會在執行 Terraform 的 "refersh" 階段時被讀取,然後 Terraform 會建置變更計劃。這保證了在製定變更計畫時 Terraform 可以使用這些資料來源的回傳資料。

如果查詢參數的表達式引用了那些只有執行部分執行變更計劃以後才能知曉的數據,例如另一個還未被創建的託管資源的輸出,那麼數據源的讀取操作會被推遲到 "apply" 階段,任何引用該資料來源輸出的表達式的值在執行到資料來源被讀取完之前都是未知的。

本地資料來源

雖然絕大多數資料來源都對應了一個透過遠端基礎設施 API 存取的外部資料來源,但也有一些特殊的資料來源僅存在於T erraform 進程內部,計算並對外輸出一些資料。

比如說,本地資料來源有 template_filelocal_fileaws_iam_policy_document 等。

本機資料來源的行為與其他資料來源完全一致,但他們輸出的結果資料只是暫時存在於 Terraform 執行時,每次計算一個新的變更計畫時這些值都會被重新計算。

資料來源的依賴關係

資料來源有著與資源一樣的依賴機制,我們也可以在 data 區塊內設定 depends_on 元參數來明確聲明依賴關係,在此不再贅述。

多資料來源實例

與資源一樣,資料來源也可以透過設定 countfor_each 元參數來建立一組多個資料來源實例,而 Terraform 也會把每個資料來源實例單獨建立並讀取對應的外部數據,對 count.index 與 each 的使用也是一樣的, 在count 與 for_each 之間選擇的原則也是一樣的。

指定特定 Provider 實例

同資源一樣,資料來源也可以透過 provider 元參數指定使用特定 Provider實 例,在此不再贅述。

生命週期

同資源不一樣,資料來源目前不可以透過設定 lifecycle 區塊來客製化生命週期,但資料來源內部 lifecycle 被設定為保留關鍵字以備將來可以支援該功能。

data
# Find the latest available AMI that is tagged with Component = web
data "aws_ami" "web" {
filter {
name = "state"
values = ["available"]
}

filter {
name = "tag:Component"
values = ["web"]
}

most_recent = true
}

引用資料來源

引用資料來源資料的語法是 data.<TYPE>.<NAME>.<ATTRIBUTE>

resource "aws_instance" "web" {
ami = data.aws_ami.web.id
instance_type = "t1.micro"
}

表達式

表達式用來在設定檔中進行一些計算。最簡單的表達式就是字面量,例如 "hello",或 5。Terraform 也支援一些更複雜的表達式,例如引用其他 resource 的輸出值、數學計算、布林條件計算,以及一些內建的函數。

Terraform 配置中許多地方都可以使用表達式,但某些特定的場景下限制了可以使用的表達式的類型,例如只準使用特定資料類型的字面量,或是禁止使用 resource 的輸出值。

我們在類型章節中已經基本介紹了類型以及類型相關的字面量,下面我們來介紹一些其他的表達式。

下標與屬性

list 和 tuple 可以透過下標存取成員,例如 local.list[3]var.tuple[2]。map 和 object 可以透過屬性存取成員,例如 local.object.attrnamelocal.map.keyname。由於 map 的 key 是使用者定義的,可能無法成為合法的 Terraform 標識符,所以訪問 map 成員時我們建議使用方括號:local.map["keyname"]

引用命名值

Terraform 中定義了多種命名值,表達式中的每一個命名值都關聯到一個具體的值,我們可以用單一命名值作為一個表達式,或是組合多個命名值來計算出一個新值。

命名值有以下種類:

  • <RESOURCE TYPE>.<NAME>:表示一個資源物件。凡是不符合後面列出的命名值模式的表達式都會被 Terraform 解釋為一個託管資源。如果資源聲明了 count 元參數,那麼該表達式表示的是一個物件實例的 list。如果資源聲明了 for_each 元參數,那麼該表達式表示的是一個物件實例的 map。
  • var.<NAME>:表示一個輸入變數
  • local.<NAME>:表示一個局部值
  • module.<MODULE_NAME>.<OUTPUT_NAME>:表示一個模組的一個輸出值
  • data.<DATA_TYPE>.<NAME>:表示一個資料來源實例。如果資料來源宣告了 count 元參數,那麼該表達式表示的是一個資料來源實例 list。如果資料來源宣告了 for_each 元參數,那麼該表達式表示的是一個資料來源實例 map。
  • path.module:表示目前模組在檔案系統中的路徑
  • path.root:表示根模組(呼叫 Terraform 命令列執行的程式碼檔案所在的模組)在檔案系統中的路徑
  • path.cwd:表示目前工作目錄的路徑。一般來說該路徑等同於 path.root,但在呼叫 Terraform 命令列時如果指定了程式碼路徑,那麼二者將會不同。
  • terraform.workspace:目前使用的 Workspace(我們在狀態管理的"狀態的隔離儲存"中介紹過)

雖然這些命名表達式可以使用 .<NAME> 號碼來存取物件的各種屬性,但實際上他們實際類型並不是我們在類型章節裡提到的 object。兩者的差別在於,object 同時支援 .<NAME> 使用或 ["<NAME>"] 兩種方式存取物件成員屬性,而上述命名表達式僅支援 .<NAME>

局部命名值

在某些特定表達式或上下文當中,有一些特殊的命名值可以被使用,他們是局部命名值。幾種比較常見的局部命名值有:

  • count.index:表達目前 count 下標序號
  • each.key:表達目前 for_each 迭代器實例
  • self:在預置器中指稱聲明預置器的資源

命名值的依賴關係

建構資源或是模組時常會使用含有命名值的表達式賦值,Terraform 會分析這些表達式並自動計算出物件之間的依賴關係。

引用資源輸出屬性

最常見的引用類型就是引用一個 resource 或 data 塊定義的物件的輸出屬性。由於這些資源與資料來源物件結構可能非常複雜,因此對它們的輸出屬性的引用表達式也可能非常複雜。

比如下面這個例子:

aws_instance
resource "aws_instance" "example" {
ami = "ami-abc123"
instance_type = "t2.micro"

ebs_block_device {
device_name = "sda2"
volume_size = 16
}
ebs_block_device {
device_name = "sda3"
volume_size = 20
}
}

aws_instance 文件列出了該類型所支援的所有輸入參數和內嵌區塊,以及對外輸出的屬性清單。所有這些不同的資源類型 Schema 都可以在引用中使用,如下所示:

  • ami 參數可以在其他地方用 aws_instance.example.ami 表達式來引用
  • id 屬性可以用 aws_instance.example.id 的表達式來引用
  • 內嵌的 ebs_block_device 參數可以透過後面會介紹的展開表達式(splat expression)來訪問,例如我們獲取所有的 ebs_block_device 列表 device_name:aws_instance.example.ebs_block_device[*].device_name
  • 在 aws_instance 類型裡的內嵌區塊並沒有任何輸出屬性,但如果 ebs_block_device 新增了一個名為 "id" 的輸出屬性,那麼可以用 aws_instance.example.ebs_block_device[*].id 表達式來存取含有所有 id 的列表
  • 有時多個內嵌區塊會各自包含一個邏輯鍵來區分彼此,類似用資源名稱存取資源,我們也可以用內嵌區塊的名字來存取特定內嵌區塊。假如 aws_instance 類型有一個假想的內嵌區塊類型 device 並規定 device 可以賦予這樣的一個邏輯鍵,那麼程式碼看起來就會是這樣的:
device "foo" {
size = 2
}
device "bar" {
size = 4
}

我們可以使用鍵來存取特定區塊的數據,例如:aws_instance.example.device["foo"].size

要取得一個 device 名稱到 device 大小的映射,可以使用 for 表達式:

{for k, device in aws_instance.example.device : k => device.size}

當一個資源宣告了 count 參數,那麼資源本身就變成了一個資源物件清單而非單一資源。這種情況下要存取資源輸出屬性,要麼使用展開表達式,要麼使用下標索引:

  • aws_instance.example[*].id:傳回所有 instance 的 id 列表
  • aws_instance.example[0].id:返回第一個 instance 的 id

當一個資源宣告了 for_each 參數,那麼資源本身就變成了一個資源物件字典而非單一資源。這種情況下要存取資源的輸出屬性,要麼使用特定鍵,要麼使用 for 表達式:

  • aws_instance.example["a"].id:傳回 "a" 對應的實例的 id
  • [for value in aws_instance.example: value.id]:傳回所有 instance 的 id

注意不像使用 count,使用 for_each 的資源集合不能直接使用展開表達式,展開表達式只能適用於列表。你可以把字典轉換成列表後再使用展開表達式:

  • values(aws_instance.example)[*].id

尚不知曉的數值

當 Terraform 在計算變更計畫時,有些資源輸出屬性無法立即求值,因為他們的值取決於遠端 API 的回傳值。比如說,有一個遠端物件可以在創建時回傳一個生成的唯一 id,Terraform 無法在創建它之前就預知這個值。

為了允許在計算變更階段就能計算含有這種值的表達式,Terraform 使用了一個特殊的"尚不知曉(unknown value)"佔位符來代替這些結果。大部分時候你不需要特意理會它們,因為 Terraform 語言會自動處理這些尚不知曉的值,比如說使兩個尚不知曉的值相加得到的會是一個尚不知曉的值。

然而,有些情況下表達式中含有尚不知曉的值會有明顯的影響:

  • count 元參數不可以為尚不知曉,因為變更計畫必須明確知道到底要維護多少個目標實例
  • 如果尚未知道的值被用於資料來源,那麼資料來源在計算變更計劃階段就無法讀取,它會被推遲到執行階段讀取。在這種情況下,在計劃階段該資料來源的一切輸出均為尚不知曉
  • 如果聲明 module 區塊時傳遞給模組輸入變數的表達式使用了尚不知曉值,那麼在模組程式碼中任何使用了該輸入變數值的表達式的值都將是尚不知曉
  • 如果模組輸出值表達式中含有尚不知曉值,任何使用該模組輸出值的表達式都將尚不知曉
  • Terraform 會嘗試驗證尚未知道值的資料類型是否合法,但仍有可能無法正確檢查資料類型,導致執行階段發生錯誤

尚不知曉值在執行 terraform plan 時會被輸出為"(not yet known)"。

算數與邏輯運算符

一個操作符是一種用以轉換或合併一個或多個表達式的表達式。操作子要嘛是把兩個值計算為第三個值,也就是二元運算子;要嘛是把一個值轉換成另一個值,也就是一元運算子。

二元運算子位於兩個表達式的中間,類似 1+2。一元操作符位於一個表達式的前面,類似 !true

Terraform 語言支援一組算數和邏輯操作符,它們的功能類似 JavaScript 或 Ruby 裡的操作符功能。

當一個表達式中含有多個操作符時,它們的優先順序時:

  1. !- (負號)
  2. \*/%
  3. +- (減號)
  4. \>>=<<=
  5. ==!=
  6. &&
  7. ||

可以使用小括號覆蓋預設優先權。如果沒有小括號,高優先權運算子會被先計算,例如 1+23 會被解釋成 1+(23) 而不是 (1+2)*3。

不同的運算子可以按它們之間相似的行為被歸納為幾組,每一組操作符都期待被給予特定類型的值。Terraform會在類型不符時嘗試進行隱式類型轉換,如果失敗則會拋錯。

算數運算符

  • a + b:返回 a 與 b 的和
  • a - b:返回 a 與 b 的差
  • a * b:返回 a 與 b 的積
  • a / b:返回 a 與 b 的商
  • a % b:返回 a 與 b 的模。此運算符一般僅在 a 與 b 是整數時有效
  • a:返回 a 與 1 的商

相等性操作符

  • a == b:如果 a 與 b 類型與值都相等返回 true,否則返回 false
  • a != b:與 == 相反

比較操作符

  • a < b:如果 a 比 b 小則為 true,否則為 false
  • a > b:如果 a 比 b 大則為 true,否則為 false
  • a <= b:若 a 比 b 小或相等則為 true,否則為 false
  • a = b:若 a 比 b 大或相等則為 true,否則為 false

邏輯運算符

  • a || ba 或 b 中有至少一個為 true 則為 true,否則為 false
  • a && ba 與比都為 true 則為 true ,否則為false
  • !a:如果 a 為 true 則為 false,如果 a 為 false 則為 true

條件式

條件式是判斷布林表達式的結果以便於在後續兩個值當中選擇一個:

condition ? true_val : false_val

如果 condition 表達式為 true,那麼結果是 true_value,反之則為 false_value。

一個常見的條件式用法是使用預設值來取代非法值:

var.a != "" ? var.a : "default-a"

如果輸入變數 a 的值是空字串,那麼結果會是 default-a,否則傳回輸入變數 a 的值。

條件式的判斷條件可以使用上述的任意運算子。供選擇的兩個值也可以是任意類型,但它們的類型必須相同,這樣 Terraform 才能判斷條件表達式的輸出類型。

調用函數

Terraform 支援在計算表達式時使用一些內建函數,函數呼叫表達式類似操作符,通用語法是:

<FUNCTION NAME>(<ARGUMENT 1>, <ARGUMENT 2>)

函數名標明了要呼叫的函數。每一個函數都定義了數量不等、類型不一的入參以及不同類型的回傳值。

有些函數定義了不定長的入參表,例如,min 函數可以接收任意多個數值類型入參,傳回其中最小的數值:

min(55, 3453, 2)

展開函數入參

如果想要把列表或元組的元素當作參數傳遞給函數,那麼我們可以使用展開符:

min([55, 2453, 2]...)

展開符號使用的是三個獨立的 . 號組成的 ... ,不是 Unicode 的省略號 。展開符是一種只能用在函數呼叫場景下的特殊語法。

有關完整的內建函數我們可能會在今後撰寫對應的章節介紹。

for 表達式

for 表達式是將一種複雜類型對應成另一種複雜類型的表達式。輸入類型值中的每一個元素都會被映射為一個或零個結果。

舉例來說,如果 var.list 是字串列表,那麼下面的表達式將會把列表元素全部轉為大寫:

[for s in var.list : upper(s)]

這裡 for 表達式迭代了 var.list 中每一個元素(就是 s),然後計算了 upper(s),最後建構了一個包含了所有 upper(s) 結果的新元組,元組內元素順序與來源列表相同。

for 表達式周圍的括號類型決定了輸出值的類型。上面的例子我們使用了方括號,所以輸出型別是元組。如果使用的是花括號,那麼輸出類型是對象,for 表達式內部冒號後面應該使用以 => 符號分隔的表達式:

{for s in var.list : s => upper(s)}

這個表達式傳回一個對象,而對象的成員屬性名稱就是來源列表中的元素,值就是對應的大寫值。

一個 for 表達式還可以包含一個可選的 if 子句用以過濾結果,這可能會減少傳回的元素數量:

[for s in var.list : upper(s) if s != ""]

被 for 迭代的也可以是物件或字典,這樣的話迭代器就會被表示為兩個臨時變數:

[for k, v in var.map : length(k) + length(v)]

最後,如果傳回類型是物件(使用花括號)那麼表達式中可以使用 ... 符號實作 group by:

{for s in var.list : substr(s, 0, 1) => s... if s != ""}

展開表達式 (Splat Expression)

展開表達式提供了一種類似 for 表達式的簡潔表達方式。比如說 var.list 包含一組對象,每個物件都有一個屬性 id,那麼讀取所有 id 的 for 表達式會是這樣:

[for o in var.list : o.id

與之等價的展開表達式是這樣的:

var.list[*].id

這個特殊的 [*] 符號迭代了列表中每一個元素,然後傳回了它們在 . 號碼右邊的屬性值。

展開表達式只能被用於列表(所以使用 for_each 參數的資源不能使用展開表達式,因為它的型別是字典)。然而,如果一個展開表達式被用於一個既不是列表又不是元組的值,那麼這個值會被自動包裝成一個單元素的列表然後被處理。

比如說, var.single_object[*].id 等價於 [var.single_object][*].id。大部分場景下這種行為沒有什麼意義,但在存取不確定是否會定義 count 參數的資源時,這種行為很有幫助,例如:

aws_instance.example[*].id

上面的表達式不論 aws_instance.example 定義了 count 與否都會傳回實例的 id 列表,這樣如果我們以後為 aws_instance.example 添加了 count 參數我們也不需要修改這個表達式。

遺留的舊有展開表達式

曾經存在另一種舊的展開表達式語法,它是一種比較弱化的展開表達式,現在應該盡量避免使用。

這種舊的展開表達式使用 .* 而不是 [*]

var.list.*.interfaces[0].name

要特別注意該表達式與現有的展開表達式結果不同,它的行為等價於:

[for o in var.list : o.interfaces][0].name

而現有 [*] 展開表達式的行為等價於:

[for o in var.list : o.interfaces[0].name]

注意兩者右邊括號的位置。

dynamic 區塊

在頂級區塊,例如 resource 區塊當中,一般只能以類似 name = expression 的形式進行一對一的賦值。大部分情況下這已經夠用了,但某些資源類型包含了可重複的內嵌區塊,無法使用表達式循環賦值:

resource  "aws_elastic_beanstalk_environment" "tfenvtest" {
name = "tf-test-name" # can use expressions here

setting {
# but the "setting" block is always a literal block
}
}

你可以用 dynamic 區塊來動態建立重複的 setting 這樣的內嵌區塊:

resource "aws_elastic_beanstalk_environment" "tfenvtest" {
name = "tf-test-name"
application = "${aws_elastic_beanstalk_application.tftest.name}"
solution_stack_name = "64bit Amazon Linux 2018.03 v2.11.4 running Go 1.12.6"

dynamic "setting" {
for_each = var.settings
content {
namespace = setting.value["namespace"]
name = setting.value["name"]
value = setting.value["value"]
}
}
}

dynamic 可以在 resourcedataprovider 和 provisioner 區塊內使用。一個 dynamic 區塊類似於 for 表達式,只不過它產生的是內嵌塊。它可以迭代一個複雜類型資料然後為每一個元素產生對應的內嵌區塊。在上面的例子裡:

  • dynamic 的標籤(也就是 "setting")確定了我們要產生的內嵌塊種類
  • for_each 參數提供了需要迭代的複雜型別值
  • iterator 參數(可選)設定了用以表示目前迭代元素的臨時變數名稱。如果沒有設定 iterator,那麼臨時變數名稱預設就是 dynamic 區塊的標籤(也就是 setting)
  • labels 參數(可選)是一個表示區塊標籤的有序列表,用以順序產生一組內嵌區塊。有 labels 參數的表達式裡可以使用暫時的 iterator 變數
  • 內嵌的 content 區塊定義了要產生的內嵌區塊的區塊體。你可以在 content 區塊內部使用臨時的 iterator 變數由於 for_each 參數可以是集合或結構化類型,所以你可以使用 for 表達式或是展開表達式來轉換一個現有集合的類型。

iterator 變數(上面的範例裡就是 setting )有兩個屬性:

  • key:迭代容器如果是 map,那就是當前元素的鍵;迭代容器如果是 list,那麼就是當前元素在 list 中的下標序號;如果是 for_each 表達式產出的 set,那麼 key 和 value 是一樣的,這時我們不應該使用 key。
  • value:當前元素的值一個 dynamic 區塊只能產生屬於目前區塊定義過的內嵌區塊參數。無法產生諸如 lifecycleprovisioner 這樣的元參數,因為 Terraform 必須在確保這些元參數求值的計算是成功的。

for_each 的值必須是不為空的 map 或 set。如果你需要根據內嵌資料結構或多個資料結構的元素組合來宣告資源實例集合,你可以使用 Terraform 表達式和函數來產生適當的值。

dynamic 區塊的最佳實踐

過度使用 dynamic 區塊會導致程式碼難以閱讀以及維護,所以我們建議只在需要建構可重複使用的模組程式碼時才使用 dynamic 區塊。盡可能手寫內嵌塊。

字串字面量

Terraform 有兩種不同的字串字面量。最通用的就是用一對雙引號包裹的字符,例如 "hello"。在雙引號之間,反斜線 \ 被用來進行轉義。Terraform 支援的轉義符有:

SequenceReplacement
\n換行
\r回車
\t製表符
"雙引號(不會截斷字串)
\反斜線
\uNNNN普通字元對映平面的Unicode字元(NNNN代表四位元16進位數)
\UNNNNNNNN補充字元映射平面的Unicode字元(NNNNNNNN代表八位元16進位數)

另一種字串表達式稱為 "heredoc" 風格,是受 Unix Shell 語言啟發。它可以使用自訂的分隔符號更清晰地表達多行字串:

<<EOT
hello
world
EOT

<< 標記後面直到行尾組成的識別碼開啟了字串,然後 Terraform 會把剩下的行都加進字串,直到遇到與識別符完全相等的字串為止。在上面的例子裡,EOT 就是識別符。任何字元都可以用作標識符,但傳統上標識符一般以 EO 起頭。上面例子裡的 EOT 代表"文本的結尾(end of text)"。

上面例子裡的 heredoc 風格字串要求內容必須對齊行頭,這在區塊內宣告時看起來會比較奇怪:

block {
value = <<EOT
hello
world
EOT
}

為了改善可讀性,Terraform 也支援縮排的 heredoc,只要把 << 改成 <<-

block {
value = <<-EOT
hello
world
EOT
}

上面的例子裡,Terraform 會以最靠近行頭的行作為基準來調整行頭縮進,得到的字串是這樣的:

hello
world

heredoc 中的反斜杠不會被解釋成轉義,而只會是簡單的反斜杠。

雙引號和 heredoc 兩種字串都支援字串模版,模版的形式是 ${...} 以及 %{...}。如果想要表達 ${ 或 %{ 的字面量,那麼可以重複第一個字元:$${ 和 %%{

字串模版

字串模版允許我們在字串中嵌入表達式,或是透過其他值動態建構字串。

插值 (Interpolation)

一個 ${...} 序列稱為插值,插值計算花括號之間的表達式的值,有必要的話將之轉換為字串,然後插入字串模版,形成最終的字串:

"Hello, ${var.name}!"

上面的例子裡,輸入變數 var.name 的值被存取後插入了字串模版,產生了最終的結果,例如:"Hello, Juan!"

命令 (Directive)

一個 %{...} 序列被稱為命令,命令可以是一個布林表達式或是對集合的迭代,類似條件表達式以及 for 表達式。有兩種指令:

  • if \<BOOL\> / else / endif 指令根據布林表達式的結果在兩個模版中選擇一個:
"Hello, %{ if var.name != "" }${var.name}%{ else }unnamed%{ endif }!"

else 部分可以省略,這樣如果布林表達結果為 false 那麼就會插入空字串。

  • for \<NAME\> in \<COLLECTION\> / endfor 指令迭代一個結構化物件或集合,用每一個元素渲染模版,然後把它們拼接起來:
<<EOT
%{ for ip in aws_instance.example.*.private_ip }
server ${ip}
%{ endfor }
EOT

for 關鍵字後緊跟的名字被用作代表迭代器元素的臨時變量,可以用來在內嵌模版中使用。

為了在不添加額外空格和換行的前提下提升可讀性,所有的模版序列都可以在首尾添加 ~ 符號。如果有 ~ 符號,那麼模版序列會去除字串左右的空白(空格以及換行)。如果 ~ 出現在頭部,那麼會去除字串左側的空白;如果出現在尾部,那麼就會去除字串右邊的空白:

<<EOT
%{ for ip in aws_instance.example.*.private_ip ~}
server ${ip}
%{ endfor ~}
EOT

在上面的例子裡,命令符後面的換行符號被忽略了,但是 server ${ip} 後面的換行符號被保留了,這確保了每一個元素產生一行輸出:

server 10.1.16.154
server 10.1.16.1
server 10.1.16.34

當使用模版指令時,我們推薦使用 heredoc 風格字串,用多行模版提升可讀性。雙引號字串內最好只使用插值。

Terraform 內插

Terraform 曾經只支援在表達式中使用插值

resource "aws_instance" "example" {
ami = var.image_id
# ...
}

這種語法是在 Terraform 0.12 後才支援的。在 Terrafor 0.11 及更早的版本中,只能被寫成這樣

resource "aws_instance" "example" {
ami = "${var.image_id}"
# ...
}